<?php
/**
 * This file is part of the Sphoof framework.
 * Copyright (c) 2010-2011 Sphoof
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code. You can also view the
 * LICENSE file online at http://www.sphoof.nl/new-bsd.txt
 *
 * @category	Sphoof
 * @copyright	Copyright (c) 2010-2011 Sphoof (http://sphoof.nl)
 * @license		http://sphoof.nl/new-bsd.txt	New BSD License
 * @package		Controller
 */

/**
 * An exception that will be thrown if the next path-segment can not be mapped.
 *
 * @package Controller
 * @subpackage Exceptions
 */
class SpNotFound extends SpException { }

/**
 * An exception that will be thrown if a combination of HTTP method and action
 * parameter is not implemented by the current controller.
 *
 * @package Controller
 * @subpackage Exceptions
 */
class SpNotImplemented extends SpException { }

/**
 * An exception that will be thrown if there are two or more action parameters
 * defined.
 *
 * @package Controller
 * @subpackage Exceptions
 */
class SpAmbigiousAction extends SpException { }

/**
 * All objects in the controller chain have to implement this interface.
 *
 * @package Controller
 */
interface SpControllerContext {
	/**
	 * Returns the remaining, unhandled part of the path from the URL
	 * (e.g. /foo/bar, or /bar).
	 *
	 * @return String
	 */
	public function path( );

	/**
	 * Creates and returns a controller.
	 *
	 * @param string $classname
	 * @return SpController
	 */
	public function create( $classname );

	/**
	 * Returns the request's HTTP method.
	 *
	 * @return string
	 */
	public function method( );
}

/**
 * SpController is the controller all controllers of the application should
 * extend.
 *
 * @package Controller
 */
abstract class SpController implements SpControllerContext {
	/**
	 * Contains the calling context.
	 * @var SpControllerContext
	 */
	protected $context;

	/**
	 * Contains the originating request.
	 * @var SpHttpRequest
	 */
	protected $request;

	/**
	 * An array of query parameter to controller-method mappings.
	 *
	 * @var Array
	 */
	protected $actions = array(
		'new' => '%sNew',
		'edit' => '%sEdit',
		'delete' => '%sDelete'
	);

	/**
	 * An array of extension to response-type mappings.
	 * 
	 * @var Array
	 */
	protected $responses = array(
		'html' => 'SpHtmlResponse',
		'htm' => 'SpHtmlResponse',
		'json' => 'SpJsonResponse',
		'xml' => 'SpXmlResponse'
	);

	/**
	 * An arbitrary variable that can be used to denote a users' identity.
	 * 
	 * @var mixed
	 */
	protected $identity = null;

	/**
	 * Decides which controller should be the one to handle the next part of the
	 * path. This method should return a string, or false if it can't handle it.
	 *
	 * @param string $name
	 * @return string
	 */
	abstract protected function map( $name );

	/**
	 * Dispatches the request through the controller chain, depending on the URL path.
	 *
	 * @param SpControllerContext $context
	 * @param SpHttpRequest $request
	 * @return SpHttpResponse
	 */
	public function dispatch( SpControllerContext $context, SpHttpRequest $request ) {
		$this->context = $context;
		$this->request = $request;
		if( ( $response = $this->preExecution( ) ) && ( $response instanceof SpHttpResponse ) ) {
			return $response;
		}
		$response = ( false !== ( $next = $this->next( ) ) ? $this->forward( $next ) : $this->execute( ) );
		$this->postExecution( $response );
		return $response;
	}

	/**
	 * Returns the remaining, unhandled part of the path, if any.
	 *
	 * @return mixed
	 */
	public function path( ) {
		return preg_replace( '~^[^/]*/?~', '', $this->context->path( ) );
	}

	/**
	 * Creates a new controller and returns it.
	 *
	 * @param String $controller
	 * @return SpController
	 */
	public function create( $controller ) {
		return $this->context->create( $controller );
	}

	/**
	 * Returns the HTTP method for the current request.
	 *
	 * @return String
	 */
	public function method( ) {
		return $this->context->method( );
	}

	/**
	 * Returns the part of the path that is currently handled.
	 *
	 * @return String
	 */
	public function name( ) {
		if( false !== ( $name = $this->matches( '~^([^/]+)/?~', $this->context->path( ) ) ) ) {
			return trim( strtolower( array_shift( $name ) ), '/' );
		}
		return false;
	}

	/**
	 * Returns a SpViewRenderer to render the templates.
	 *
	 * @return SpViewRenderer
	 */
	public function view( ) {
		return $this->context->view( );
	}

	/**
	 * Renders and returns a template.
	 *
	 * @param string $template
	 * @param array $params
	 * @return string
	 */
	protected function render( $template, Array $params = array( ) ) {
		if( 0 === preg_match( '~\.([a-z0-9]){2,6}$~i', $template ) ) {
			$template = sprintf( '%s.%s', $template, $this->request->url( )->extension( ) );
		}
		return $this->view( )->render( $template, ( array_merge( $this->parameters( ), $params ) ) );
	}

	/**
	 * Gets all parameters of the controller and returns them in an array.
	 *
	 * @return Array
	 */
	protected function parameters( ) {
		$return = array( );
		foreach( $this as $key => $value ) {
			$return[$key] = $value;
		}
		return $return;
	}

	/**
	 * Returns the first next unhandled part of the path, if present.
	 *
	 * @return String
	 */
	protected function next( ) {
		return false !== ( $next = $this->matches( '~^[^/]+~', $this->path( ) ) ) ? strtolower( array_shift( $next ) ) : false;
	}

	/**
	 * Forward the request to the controller mapped to the next part of the path.
	 *
	 * @param String $classname
	 * @return SpResponse
	 */
	protected function forward( $next ) {
		if( ( $classname = $this->map( $next ) ) && ( $controller = $this->create( $classname ) ) ) {
			return $controller->dispatch( $this, $this->request );
		}
		throw new SpNotFound( sprintf( 'Unable to map URL path-segment "%s" to a controller', $next ) );
	}

	/**
	 * A convenience method that will always be executed, before deciding what 
	 * happens to the request. This method may be overwritten by a client class.
	 * 
	 * @return null
	 */
	protected function preExecution( ) {
		return;
	}

	/**
	 * A convenience method that will always be executed, regardless of what
	 * happened to the request. This method may be overwritten by a client class.
	 *
	 * @return void
	 */
	protected function postExecution( SpHttpResponse $response ) {
		return;
	}

	/**
	 * Executes the request and returns a response.
	 *
	 * @return SpResponse
	 */
	protected function execute( ) {
		$action = $this->determineAction( strtolower( $this->context->method( ) ) );
		if( method_exists( $this, $action ) ) {
			return $this->respond( $this->$action( ) );
		}
		throw new SpNotImplemented( sprintf( 'Call to unimplemented action "%s"', $action ) );
	}

	/**
	 * Determines which controller-action should be called, based on a
	 * combination of the HTTP method and the actions array.
	 *
	 * @param string $method
	 * @return string
	 */
	protected function determineAction( $method ) {
		foreach( $this->actions as $parameter => $action ) {
			if( false !== $this->request->query( $parameter, false ) ) {
				if( isset( $finalaction ) ) {
					throw new SpAmbigiousAction( 'Multiple actions are set.' );
				}
				$finalaction = sprintf( $action, $method );
			}
		}
		return isset( $finalaction ) ? $finalaction : $method;
	}

	/**
	 * Returns the given response, wrapped in a SpHttpResponse object. This method
	 * will also set the correct content-type.
	 *
	 * @param mixed $response
	 * @return SpHttpResponse
	 */
	protected function respond( $response ) {
		if( false === ( $response instanceof SpHttpResponse ) ) {
			$response = new SpHttpResponse( $response );
		}
		return $this->contentType( $response );
	}

	/**
	 * Determines the appropriate content-type and sets it upon the response.
	 *
	 * @param SpHttpResponse $response
	 * @return SpHttpResponse
	 */
	protected function contentType( SpHttpResponse $response ) {
		$extension = $this->request->url( )->extension( );
		if ( isset( $this->responses[$extension] ) ) {
			$type = new $this->responses[$extension] ( );
			$type->headers( $response );
		}
		return $response;
	}

	/**
	 * Returns a redirecting response, which will redirect the user to another
	 * location.
	 *
	 * @param string $url
	 * @param int $status
	 * @return SpRedirectingResponse
	 */
	protected function redirect( $url, $status = 303 ) {
		return new SpRedirectingResponse( $url, $status );
	}

	/**
	 * Matches a pattern on a subject and returns an array of matches or the value of $default if there were no matches.
	 *
	 * @param string $pattern
	 * @param string $subject
	 * @param mixed $default
	 * @return mixed
	 */
	protected function matches( $pattern, $subject, $default = false ) {
		return ( 0 !== preg_match( $pattern, $subject, $matches) ) ? (array) $matches : $default;
	}

	/**
	 * Returns the identity of the current user, if set. If this controller
	 * hasn't set the identity, it will ask the context.
	 *
	 * @return mixed
	 */
	protected function identity( ) {
		if( isset( $this->identity ) ) {
			return $this->identity;
		}
		return $this->context->identity( );
	}
}

/**
 * Used to bootstrap the application by wiring classes.
 *
 * @package Controller
 */
class SpBootstrap implements SpControllerContext {
	/**
	 * An object that knows how to create subsequent controllers.
	 *
	 * @var SpCanCreateController
	 */
	protected $creator;

	/**
	 * A representation of the users' originating HTTP request.
	 *
	 * @var SpHttpRequest
	 */
	protected $request;

	/**
	 * Contains the interface with the View layer, an instance of SpViewRenderer.
	 *
	 * @var SpViewRenderer
	 */
	protected $view;

	/**
	 * A dependency injection container.
	 *
	 * @var SpContainer
	 */
	protected $container;

	/**
	 * An arbitrairy variable that represents a users' identity.
	 * 
	 * @var mixed
	 */
	protected $identity;

	/**
	 * Run the application by handing off the request to the first controller.
	 *
	 * @param String $controller
	 * @return SpHttpResponse
	 */
	public function run( $controller ) {
		return $this->create( $controller )->dispatch( $this, $this->request( ) );
	}

	/**
	 * Create a controller.
	 *
	 * @param string $controller
	 * @return SpController
	 */
	public function create( $controller ) {
		return $this->creator( )->create( $controller );
	}

	/**
	 * Returns a representation of the users' HTTP request.
	 *
	 * @return SpHttpRequest
	 */
	public function request( ) {
		if( null === $this->request ) {
			$this->request = new SpHttpRequest( );
		}
		return $this->request;
	}

	/**
	 * Returns the path of the URL, excluding the request's extension, relative
	 * to the applications root directory.
	 *
	 * @return string
	 */
	public function path( ) {
		return preg_replace( '~\.([a-z0-9]){2,4}$~i', '', $this->request( )->url( )->path( ) );
	}

	/**
	 * Returns the request's HTTP method.
	 *
	 * @return method
	 */
	public function method( ) {
		return $this->request( )->method( );
	}

	/**
	 * Returns the SpView implementation.
	 *
	 * @return SpView
	 */
	public function view( ) {
		if( null === $this->view ) {
			$this->view = new SpPhpRenderer( );
		}
		return $this->view;
	}

	/**
	 * Sets the creator that will be used to create all controller instances.
	 *
	 * @param SpCanCreateController $creator
	 * @return SpBootstrap
	 */
	public function setCreator( SpCanCreateController $creator ) {
		$this->creator = $creator;
		return $this;
	}

	/**
	 * Returns the creator used to create all controller instances. If the creator is not overwritten
	 * by setCreator, it will create a SpInjectingCreator by default.
	 *
	 * @return SpCanCreateController
	 */
	protected function creator( ) {
		if( null === $this->creator ) {
			$this->creator = new SpInjectingCreator( $this->container( ) );
		}
		return $this->creator;
	}

	/**
	 * Returns an instance of SpContainer that will be used to create
	 * controllers and their dependencies.
	 *
	 * @return SpContainer
	 */
	public function container( ) {
		if( null === $this->container ) {
			$this->container = new SpContainer( );
		}
		return $this->container;
	}

	/**
	 * Returns the set identity, if any.
	 *
	 * @return mixed
	 */
	public function identity( ) {
		return $this->identity;
	}

	/**
	 * Sets the identity on the controller chain.
	 *
	 * @param mixed $identity
	 * @return SpBootstrap
	 */
	public function setIdentity( $identity ) {
		$this->identity = $identity;
		return $this;
	}
}

/**
 * An interface that all objects that can build controllers should implement.
 * @package Controller
 */
interface SpCanCreateController {
	/**
	 * Creates the controller and returns it.
	 *
	 * @param string $classname
	 * @return SpController
	 */
	public function create( $classname );
}

/**
 * This class will create controllers by means of Sphoof's dependency injection
 * container.
 *
 * @package Controller
 */
class SpInjectingCreator implements SpCanCreateController {
	/**
	 * The Dependency Injection container.
	 *
	 * @var SpContainer
	 */
	protected $container;

	/**
	 * Constructs the injecting container.
	 *
	 * @param SpContainer $container
	 */
	public function __construct( SpContainer $container ) {
		$this->container = $container;
	}

	/**
	 * Creates an object of the class $classname and recursively satisfies
	 * all of it's constructor parameters.
	 *
	 * @param string $classname
	 * @return SpController
	 */
	public function create( $classname ) {
		return $this->container->create( $classname );
	}
}